﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Net;
using System.Runtime.Serialization;
using System.Security.Cryptography;
using System.Security.Permissions;
using System.Text;
using System.Xml;

[assembly: CLSCompliant(true)]
namespace SmugMugApiWrapper
{
    public class SmugMugApi
    {

        #region Private Variables

        private const string INVALID_KEY = "INVALID_KEY";
        private const int ALBUM_DOES_NOT_EXIST = -1;

        private const int CATEGORY_DOES_NOT_EXIST = -1;
        private const int SUBCATEGORY_DOES_NOT_EXIST = CATEGORY_DOES_NOT_EXIST;
        private const int INVALID_LOGIN = 1;

        private const int ERROR_INVALID_USER = 4;
        private const int ERROR_SYSTEM_ERROR = 5;
        private const int ERROR_INVALID_CATEGORY = 8;
        private const int ERROR_NO_SUBCATEGORIES = 15;

        private string _sessionKey = INVALID_KEY;
        private List<SmugMugAlbumInfo> _albums;
        private Dictionary<int, string> _categories;
        private Dictionary<int, string> _subCategories;

        private const string _serviceURL = "http://api.smugmug.com/hack/rest/1.2.0/";
        private const string _secureServiceURL = "https://api.smugmug.com/hack/rest/1.2.0/";
        private const string _uploadService = "http://upload.smugmug.com/";
        private string _apiKey = "RRHNuELVGn3NU57n64kM5UJALkYg6KwE";
        private string _userAgent = "SmugMugAPIWrapper v. 1.0.0.0";


        #endregion

        #region Constructors and destructor
        /// <summary>
        /// SmugMugAPI Constructor
        /// </summary>
        /// <param name="userName">user log in as (usually in the form user@example.com)</param>
        /// <param name="userPassword">password</param>
        public SmugMugApi(string userName, string userPassword)
        {
            _sessionKey = GetSessionKey(userName, userPassword);
        }
        /// <summary>
        /// SmugMugAPI Constructor
        /// </summary>
        /// <param name="apiKey">If you are writing a fullblown application, you can specify your own api key (obtained from smugmug) here</param>
        /// <param name="userAgent">If you are writing a fullblown application, and use your own api key, you need to use your own userAgent too (will be passed as a header to the smugmug servers)</param>
        /// <param name="userName">user log in as (usually in the form user@example.com)</param>
        /// <param name="userPassword">password</param>
        public SmugMugApi(string apiKey, string userAgent, string userName, string userPassword)
        {
            _apiKey = apiKey;
            _userAgent = userAgent;
            _sessionKey = GetSessionKey(userName, userPassword);
        }

        ~SmugMugApi()
        {
            Trace.WriteLine("In ~SmugMugAPI");
            logout();
        }

        #endregion

        #region Private methods

        private void logout()
        {
            if (_sessionKey != INVALID_KEY)
            {
                Dictionary<string, string> logoutParams = new Dictionary<string, string>();
                logoutParams.Add("SessionID", _sessionKey);
                Trace.WriteLine("Logging off");
                string sessionResponse = SmugMugRequest(true, false, "smugmug.logout", logoutParams);
                XmlDocument x = new XmlDocument();
                if (IsRESTError(sessionResponse, out x))
                {
                    throw new SmugMugApiException(x);
                }
                else
                {
                    Trace.Write("Successful logoff");
                }
                _sessionKey = INVALID_KEY;
            }
        }
        private bool IsValidSession()
        {
            return (!String.IsNullOrEmpty(_sessionKey) && _sessionKey != INVALID_KEY);
        }
        private string GetSessionKey(string user, string pass)
        {

            Dictionary<string, string> loginParams = new Dictionary<string, string>();
            string s = INVALID_KEY;
            XmlDocument x = new XmlDocument();

            loginParams.Add("EmailAddress", user);
            loginParams.Add("Password", pass);

            string sessionResponse = SmugMugRequest(false, true, "smugmug.login.withPassword", loginParams);

            if (!String.IsNullOrEmpty(sessionResponse))
            {
                if (IsRESTError(sessionResponse, out x))
                {
                    throw new SmugMugApiException(x);
                }
                else
                {
                    s = x.SelectSingleNode("/rsp/Login/Session").Attributes.GetNamedItem("id").InnerText;
                    Trace.WriteLine("session id = " + s);
                }
            }
            return s;

        }
        private bool IsValidAlbum(string albumName)
        {
            SmugMugAlbumInfo album = GetAlbumInfoByName(albumName);
            return album != null && album.AlbumID > 0;
        }
        private bool IsValidCategory(string categoryName)
        {
            int category = GetCategoryIDByName(categoryName);
            return category > CATEGORY_DOES_NOT_EXIST;

        }
        private static bool IsImplementedMethod(string method)
        {

            bool implemented = false;

            if (method == "smugmug.login.withPassword" ||
                method == "smugmug.logout" ||
                method == "smugmug.albums.get" ||
                method == "smugmug.logout" ||
                method == "smugmug.categories.get" ||
                method == "smugmug.images.get" ||
                method == "smugmug.images.getURLs" ||
                method == "smugmug.albums.create" ||
                method == "smugmug.images.getInfo" ||
                method == "smugmug.subcategories.get" ||
                method == "smugmug.images.getEXIF" ||
                method == "smugmug.albums.delete" ||
                method == "smugmug.images.changeSettings" ||
                method == "smugmug.images.delete"
                )
                implemented = true;

            Trace.WriteLineIf(!implemented, "method " + method + " isn't implemented");
            return implemented;
        }
        private static bool IsValidMethod(string method)
        {

            bool valid = false;
            //1.2 methods
            if (method == "smugmug.login.withPassword" ||
                 method == "smugmug.login.withHash" ||
                 method == "smugmug.login.anonymously" ||
                 method == "smugmug.logout" ||
                 method == "smugmug.users.getTree" ||
                 method == "smugmug.users.getTransferStats" ||
                 method == "smugmug.albums.get" ||
                 method == "smugmug.albums.getInfo" ||
                 method == "smugmug.albums.create" ||
                 method == "smugmug.albums.changeSettings" ||
                 method == "smugmug.albums.reSort" ||
                 method == "smugmug.albums.delete" ||
                 method == "smugmug.albums.getStats" ||
                 method == "smugmug.albumtemplates.get" ||
                 method == "smugmug.images.get" ||
                 method == "smugmug.images.getURLs" ||
                 method == "smugmug.images.getInfo" ||
                 method == "smugmug.images.getEXIF" ||
                 method == "smugmug.images.changeSettings" ||
                 method == "smugmug.images.changePosition" ||
                 method == "smugmug.images.upload" ||
                 method == "smugmug.images.uploadFromURL" ||
                 method == "smugmug.images.delete" ||
                 method == "smugmug.images.getStats" ||
                 method == "smugmug.categories.get" ||
                 method == "smugmug.categories.create" ||
                 method == "smugmug.categories.delete" ||
                 method == "smugmug.categories.rename" ||
                 method == "smugmug.subcategories.get" ||
                 method == "smugmug.subcategories.getAll" ||
                 method == "smugmug.subcategories.create" ||
                 method == "smugmug.subcategories.delete" ||
                 method == "smugmug.subcategories.rename")
                valid = true;

            Trace.WriteLineIf(!valid, method + " is not a valid method");
            return valid;
        }
        private static void DownloadFile(string imageURL, string downloadLocation)
        {


            string imageFileName = imageURL.Substring(imageURL.LastIndexOf("/", StringComparison.CurrentCulture) + 1);

            using (WebClient wc = new WebClient())
            {
                if (!Directory.Exists(downloadLocation))
                {
                    //let someone else deal with the problem if you can't 
                    //create the directory.
                    Trace.WriteLine("Creating directory " + downloadLocation);
                    Directory.CreateDirectory(downloadLocation);
                }

                Trace.WriteLine("Downloading " + imageURL);
                //I hope you have access to where you are going to put the file!
                wc.DownloadFile(imageURL, Path.GetFullPath(downloadLocation) + "\\" + imageFileName);
            }

        }
        private static string GetMD5Checksum(string fileName)
        {

            MD5 md5 = MD5.Create();
            StringBuilder sb = new StringBuilder();

            using (FileStream s = new FileStream(fileName, FileMode.Open, FileAccess.Read))
            {
                foreach (byte b in md5.ComputeHash(s))
                    sb.Append(b.ToString("x2").ToLower());
            }

            return sb.ToString();
        }
        private static byte[] GetImageData(string fileName)
        {

            using (FileStream f = new FileStream(fileName, FileMode.Open, FileAccess.Read))
            {
                FileInfo fi = new FileInfo(fileName);

                if (fi.Length > int.MaxValue) throw new InvalidOperationException("File length > int.MaxValue");

                byte[] dataArray = new byte[fi.Length];
                f.Read(dataArray, 0, (int)fi.Length);

                Trace.WriteLine("image is " + fi.Length + " bytes");
                return dataArray;
            }

        }
        private SmugMugAlbumInfo GetAlbumInfoByName(string albumName)
        {

            if (_albums == null)
                _albums = GetAlbums();

            foreach (SmugMugAlbumInfo i in _albums)
            {
                if (i.AlbumName.ToUpper() == albumName.ToUpper())
                    return i;
            }

            return null;
        }
        private int GetCategoryIDByName(string categoryName)
        {

            if (_categories == null)
                _categories = GetCategories();

            if (_categories.ContainsValue(categoryName))
            {
                foreach (KeyValuePair<int, string> kvp in _categories)
                {
                    if (kvp.Value.ToUpperInvariant() == categoryName.ToUpperInvariant()) return kvp.Key;
                }
            }
            return CATEGORY_DOES_NOT_EXIST;

        }
        private int GetSubCategoryIDByName(int category, string subCategoryName)
        {

            if (_subCategories == null)
                _subCategories = GetSubCategories(category);

            if (_subCategories.ContainsValue(subCategoryName))
            {
                foreach (KeyValuePair<int, string> kvp in _categories)
                {
                    if (kvp.Value.ToUpperInvariant() == subCategoryName.ToUpperInvariant()) return kvp.Key;
                }
            }
            return SUBCATEGORY_DOES_NOT_EXIST;

        }
        private static string GetXmpXmlDocFromImage(string filename)
        {

            /*
             * I found this code at http://www.shahine.com/omar/ReadingXMPMetadataFromAJPEGUsingC.aspx
             */
            string contents = string.Empty;
            string xmlPart = string.Empty;
            string beginCapture = "<rdf:RDF";
            string endCapture = "</rdf:RDF>";
            int beginPos;
            int endPos;

            using (System.IO.StreamReader sr = new System.IO.StreamReader(filename))
            {
                contents = sr.ReadToEnd();
                Debug.Write(contents.Length + " chars" + Environment.NewLine);
                sr.Close();
            }

            beginPos = contents.IndexOf(beginCapture, 0);

            if (beginPos > -1)
            {
                endPos = contents.IndexOf(endCapture, 0);

                Debug.Write("xml found at pos: " + beginPos.ToString() + " - " + endPos.ToString());

                xmlPart = contents.Substring(beginPos, (endPos - beginPos) + endCapture.Length);

                Debug.Write("Xml len: " + xmlPart.Length.ToString());
            }

            return xmlPart;
        }
        private void GetFileMetadata(string xmpXmlDoc, out List<string> keywords, out string description)
        {

            /*
             * I found this code at http://www.shahine.com/omar/ReadingXMPMetadataFromAJPEGUsingC.aspx
             */

            keywords = new List<string>();
            description = "";

            XmlDocument doc = new XmlDocument();

            if(string.IsNullOrEmpty(xmpXmlDoc))
                return;

            try
            {
                doc.LoadXml(xmpXmlDoc);
            }
            catch (Exception ex)
            {
                throw new ApplicationException("An error occured while loading XML metadata from image. The error was: " + ex.Message);
            }

            try
            {
                doc.LoadXml(xmpXmlDoc);

                XmlNamespaceManager NamespaceManager = new XmlNamespaceManager(doc.NameTable);
                NamespaceManager.AddNamespace("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
                NamespaceManager.AddNamespace("exif", "http://ns.adobe.com/exif/1.0/");
                NamespaceManager.AddNamespace("x", "adobe:ns:meta/");
                NamespaceManager.AddNamespace("xap", "http://ns.adobe.com/xap/1.0/");
                NamespaceManager.AddNamespace("tiff", "http://ns.adobe.com/tiff/1.0/");
                NamespaceManager.AddNamespace("dc", "http://purl.org/dc/elements/1.1/");

                // get ratings
                XmlNode xmlNode = doc.SelectSingleNode("/rdf:RDF/rdf:Description/xap:Rating", NamespaceManager);

                // Alternatively, there is a common form of RDF shorthand that writes simple properties as
                // attributes of the rdf:Description element.
                if (xmlNode == null)
                {
                    xmlNode = doc.SelectSingleNode("/rdf:RDF/rdf:Description", NamespaceManager);
                    xmlNode = xmlNode.Attributes["xap:Rating"];
                }

                if (xmlNode != null)
                {
                    //this.Rating = Convert.ToInt32(xmlNode.InnerText);
                }

                // get keywords
                xmlNode = doc.SelectSingleNode("/rdf:RDF/rdf:Description/dc:subject/rdf:Bag", NamespaceManager);

                if (xmlNode != null)
                {

                    foreach (XmlNode li in xmlNode)
                    {
                        keywords.Add(li.InnerText);
                    }
                }

                // get description
                xmlNode = doc.SelectSingleNode("/rdf:RDF/rdf:Description/dc:description/rdf:Alt", NamespaceManager);

                if (xmlNode != null)
                {
                    description = xmlNode.ChildNodes[0].InnerText;
                }

            }
            catch (Exception ex)
            {
                throw new ApplicationException("Error occured while readning meta-data from image. The error was: " + ex.Message);
            }
            finally
            {
                doc = null;
            }
        }
        private Dictionary<int, string> GetSubCategories(int categoryID)
        {

            Dictionary<string, string> p = new Dictionary<string, string>();
            Dictionary<int, string> subcategories = new Dictionary<int, string>();
            XmlDocument x = new XmlDocument();

            if (!IsValidSession())
                throw new InvalidOperationException("Invalid Session.  You must logon first");



            p.Add("SessionID", _sessionKey);
            p.Add("CategoryID", categoryID.ToString());

            string s = SmugMugRequest(true, false, "smugmug.subcategories.get", p);


            if (IsRESTError(s, out x))
            {
                SmugMugApiException ex = new SmugMugApiException(x);
                if (ex.ErrorCode != ERROR_NO_SUBCATEGORIES)
                    throw ex;
            }

            XmlNodeList nodes = x.GetElementsByTagName("SubCategory");

            for (int i = 0; i < nodes.Count; i++)
            {
                int subCategoryID = Convert.ToInt32(nodes[i].Attributes.GetNamedItem("id").InnerText, CultureInfo.CurrentCulture);
                string subCategoryName = nodes[i].Attributes.GetNamedItem("Title").InnerText;
                subcategories.Add(subCategoryID, subCategoryName);
            }

            return subcategories;

        }
        private string SmugMugBinaryUpload(string fileName, PostPicturesToSmugMugArguments arguments)
        {

            if (!File.Exists(fileName))
                throw new FileNotFoundException();

            if (String.IsNullOrEmpty(arguments.AlbumName))
                throw new ArgumentException("Must supply AlbumName");

            if (!IsValidSession())
            {
                throw new InvalidOperationException("Invalid Session.  You must logon first.");
            }

            FileInfo fi = new FileInfo(fileName);

            HttpWebRequest uploadRequest = (HttpWebRequest)WebRequest.Create(_uploadService + fi.Name);


            uploadRequest.ContentLength = fi.Length;
            uploadRequest.ContentType = "image/jpeg";
            uploadRequest.Headers.Add("Content-MD5", GetMD5Checksum(fileName));
            uploadRequest.Headers.Add("X-Smug-SessionID", _sessionKey);
            uploadRequest.Headers.Add("X-Smug-Version", arguments.Version);
            uploadRequest.Headers.Add("X-Smug-ResponseType", arguments.ResponseType);

            //should I create the album if it isn't already there? 
            SmugMugAlbumInfo albumInfo = GetAlbumInfoByName(arguments.AlbumName);
            if (albumInfo == null || albumInfo.AlbumID == ALBUM_DOES_NOT_EXIST)
            {
                CreateAlbumArguments args = new CreateAlbumArguments();
                args.Name = arguments.AlbumName;
                albumInfo = CreateAlbum(args);
            }




            string description = "";
            List<string> keywords = new List<string>();
            GetFileMetadata(GetXmpXmlDocFromImage(fileName), out keywords, out description);


            if (string.IsNullOrEmpty(arguments.Keywords))
            {
                foreach (string s in keywords)
                {

                    /*I dont want to send my kids names 
                     * up to smugmug as a keyword, but 
                     * I've not figured out how to make this generic
                     * yet
                     */
                    if (!s.ToUpper().Contains("LOYTTY"))
                        arguments.Keywords += s + ";";
                }
            }

            if (string.IsNullOrEmpty(arguments.Caption))
            {
                arguments.Caption = description;
            }

            uploadRequest.Headers.Add("X-Smug-AlbumID", Convert.ToString(albumInfo.AlbumID, CultureInfo.CurrentCulture));
            uploadRequest.Headers.Add("X-Smug-AlbumKey", Convert.ToString(albumInfo.AlbumKey, CultureInfo.CurrentCulture));

            if (!String.IsNullOrEmpty(arguments.FileName))
                uploadRequest.Headers.Add("X-Smug-FileName", arguments.FileName);

            if (!String.IsNullOrEmpty(arguments.Caption))
                uploadRequest.Headers.Add("X-Smug-Caption", arguments.Caption);

            if (!String.IsNullOrEmpty(arguments.Keywords))
                uploadRequest.Headers.Add("X-Smug-Keywords", arguments.Keywords);

            if (!String.IsNullOrEmpty(arguments.ImageId))
                uploadRequest.Headers.Add("X-Smug-ImageID", arguments.ImageId);

            if (!String.IsNullOrEmpty(arguments.Latitude))
                uploadRequest.Headers.Add("X-Smug-Latitude", arguments.Latitude);

            if (!String.IsNullOrEmpty(arguments.Longitude))
                uploadRequest.Headers.Add("X-Smug-Longitude", arguments.Longitude);

            if (!String.IsNullOrEmpty(arguments.Altitude))
                uploadRequest.Headers.Add("X-Smug-Latitude", arguments.Altitude);


            uploadRequest.Method = "PUT";
            uploadRequest.UserAgent = _userAgent;
            uploadRequest.Timeout = 5 * (60 * 1000);

            Trace.WriteLine("uploading " + fileName);

            using (Stream dataStream = uploadRequest.GetRequestStream())
            {
                Byte[] arr = GetImageData(fileName);
                dataStream.Write(arr, 0, (int)arr.Length);
                dataStream.Close();
            }

            return GetResponse(uploadRequest);


        }
        private string SmugMugRequest(bool withSession, bool useSSL, string method, Dictionary<string, string> parameters)
        {

            string service = _serviceURL;
            string request = "?APIKey=" + _apiKey + "&method=" + method;

            if (!IsValidMethod(method))
                throw new ArgumentException(method + " is not a valid method");

            if (!IsImplementedMethod(method))
                throw new NotImplementedException(method + " is not implemented in this version of SmugMugAPI");

            if (withSession)
            {
                if (!IsValidSession()) throw new InvalidOperationException("Invalid Session. Either call with withSession set to false or get a valid session.");

                request += "&SessionID=" + _sessionKey;
            }

            if (useSSL)
                service = _secureServiceURL;


            foreach (KeyValuePair<string, string> p in parameters)
            {
                request += "&" + p.Key + "=" + p.Value;
            }

            HttpWebRequest smugMugRequest = (HttpWebRequest)WebRequest.Create(service + request);
            smugMugRequest.Timeout = 5 * (60 * 1000); //what is a good value to put here?
            smugMugRequest.UserAgent = _userAgent;

            Trace.WriteLine("processing request " + method);

            return GetResponse(smugMugRequest);
        }
        private static string GetResponse(HttpWebRequest smugMugRequest)
        {
            byte[] buffer = new byte[8192];
            StringBuilder sb = new StringBuilder();


            try
            {
                using (HttpWebResponse smugMugResponse = (HttpWebResponse)smugMugRequest.GetResponse())
                {
                    
                    using (Stream smugMugResponseStream = smugMugResponse.GetResponseStream())
                    {

                        string temp = null;

                        int count = 0;
                        

                        do
                        {
                            count = smugMugResponseStream.Read(buffer, 0, buffer.Length);
                            if (count != 0)
                            {
                                temp = Encoding.UTF8.GetString(buffer, 0, count);
                                sb.Append(temp);
                            }
                        } while (count > 0);
                        smugMugResponseStream.Close();
                    }
                    smugMugResponse.Close();
                }
            }
            catch
            {
                throw;
            }

            return sb.ToString();
        }
        private static bool IsRESTError(string input, out XmlDocument x)
        {
            x = new XmlDocument();
            x.LoadXml(input);
            return x.DocumentElement.Attributes.GetNamedItem("stat").InnerText.ToLower(CultureInfo.CurrentCulture) != "ok";
        }

        #endregion

        #region Public methods

        public bool ChangeSmugmugImageSettings(ChangeSettingsArguments csa) {

            bool returnValue = false;

            XmlDocument x = new XmlDocument();

            if (!IsValidSession())
                throw new InvalidOperationException("you must logon first");

            Dictionary<string, string> changeArgs = new Dictionary<string, string>();
            changeArgs.Add("ImageID", Convert.ToString(csa.ImageID));
            changeArgs.Add("Caption", csa.Caption);
            changeArgs.Add("Keywords", csa.ImageKeywords);
            changeArgs.Add("Hidden",csa.Hidden?"True":"False");

            string s = SmugMugRequest(true, true, "smugmug.images.changeSettings", changeArgs);

            if(IsRESTError(s, out x)){
                throw new SmugMugApiException(x);
            }else{
                returnValue = true;
            }
            return returnValue;
        
        }

        public bool DeleteSmugMugImage(DeleteImageArguments dia) {

            bool returnValue = false;

            XmlDocument x = new XmlDocument();

            if (!IsValidSession())
                throw new InvalidOperationException("you must logon first");

            Dictionary<string, string> deleteArgs = new Dictionary<string, string>();
            deleteArgs.Add("ImageID", dia.ImageId.ToString(CultureInfo.CurrentCulture));
            deleteArgs.Add("ImageKey", dia.ImageKey);

            string s = SmugMugRequest(true, true, "smugmug.images.delete", deleteArgs);

            if (IsRESTError(s, out x))
            {
                throw new SmugMugApiException(x);
            }
            else {
                returnValue = true;
            }

            return returnValue;
        
        }

        public bool DeleteSmugMugAlbum(string albumName) {

            //this is very dangerous, and hence not documented, becase you can have many albums with the same name, but 
            //different IDs, and this function will happily delete all of them.  Dont use it unless you're sure.

            
            XmlDocument x = new XmlDocument();

            if (!IsValidSession())
                throw new InvalidOperationException("you must logon first");

            bool keepGoing = true;

            while (keepGoing)
            {
                SmugMugAlbumInfo i = GetAlbumInfoByName(albumName);

                if (i == null || i.AlbumID <= 0)
                {
                    keepGoing = false;
                    //throw new ArgumentException(albumName + " does not exist");
                }
                else
                {

                    Dictionary<string, string> deleteArgs = new Dictionary<string, string>();

                    deleteArgs.Add("SessionID", _sessionKey);
                    deleteArgs.Add("AlbumID", Convert.ToString(i.AlbumID));
                    deleteArgs.Add("AlbumKey", i.AlbumKey);

                    string s = SmugMugRequest(true, true, "smugmug.albums.delete", deleteArgs);

                    if (IsRESTError(s, out x))
                        throw new SmugMugApiException(x);

                    _albums = null;
                }
            }

            return true;



        }
        /// <summary>
        /// Retrieves information about the EXIF data on an image
        /// this is meant to be used by SmugMugIMagesInfo and SmugMugIMages
        /// classes, as opposed to being called directly
        /// </summary>
        /// <param name="arguments"></param>
        /// <returns></returns>
        public SmugMugImageEXIFData GetEXIFData(GetEXIFArguments arguments)
        {

            Dictionary<string, string> parameters = new Dictionary<string, string>();
            XmlDocument x = new XmlDocument();

            if (!IsValidSession())
                throw new InvalidOperationException("Invalid Session.  You must logon first");

            parameters.Add("SessionID", _sessionKey);
            parameters.Add("ImageID", arguments.ImageID.ToString());
            parameters.Add("ImageKey", arguments.ImageKey.ToString());

            if (!String.IsNullOrEmpty(arguments.Password))
                parameters.Add("Password", arguments.Password);

            string s = SmugMugRequest(true, true, "smugmug.images.getEXIF", parameters);

            if (IsRESTError(s, out x))
                throw new SmugMugApiException(x);

            return new SmugMugImageEXIFData(x);



        }

        public List<SmugMugAlbumInfo> GetAlbums()
        {

            Dictionary<string, string> p = new Dictionary<string, string>();
            List<SmugMugAlbumInfo> info = new List<SmugMugAlbumInfo>();
            XmlDocument x = new XmlDocument();


            if (!IsValidSession())
                throw new InvalidOperationException("Invalid Session.  You must log on first");

            p.Add("SessionID", _sessionKey);

            string s = SmugMugRequest(true, false, "smugmug.albums.get", p);

            if (IsRESTError(s, out x))
                throw new SmugMugApiException(x);

            XmlNodeList nodes = x.GetElementsByTagName("Album");

            for (int i = 0; i < nodes.Count; i++)
            {
                int albumID = Convert.ToInt32(nodes[i].Attributes.GetNamedItem("id").InnerText, CultureInfo.CurrentCulture);
                string albumKey = nodes[i].Attributes.GetNamedItem("Key").InnerText;
                string albumName = nodes[i].Attributes.GetNamedItem("Title").InnerText;

                SmugMugCategoryInfo categoryInfo=null;
                SmugMugSubCategoryInfo subCategoryInfo=null;
                string catid = string.Empty;
                string catname = string.Empty;
                string subid = string.Empty;
                string subname = string.Empty;

                if (nodes[i].HasChildNodes)
                {
                    if (null != nodes[i].SelectSingleNode("Category"))
                    {
                        catid = nodes[i].SelectSingleNode("Category").Attributes.GetNamedItem("id").InnerText;
                        catname = nodes[i].SelectSingleNode("Category").Attributes.GetNamedItem("Name").InnerText;
                    }

                    if (null != nodes[i].SelectSingleNode("SubCategory"))
                    {
                        subid = nodes[i].SelectSingleNode("SubCategory").Attributes.GetNamedItem("id").InnerText;
                        subname = nodes[i].SelectSingleNode("SubCategory").Attributes.GetNamedItem("Name").InnerText;
                    }

                    if (catid != string.Empty && catname != string.Empty)
                    {
                        categoryInfo = new SmugMugCategoryInfo(Convert.ToInt32(catid), catname);
                    }

                    if (subid != string.Empty && subname != string.Empty)
                    {
                        subCategoryInfo = new SmugMugSubCategoryInfo(Convert.ToInt32(subid), subname);
                    }

                }

                info.Add(new SmugMugAlbumInfo(albumID, albumKey, albumName,categoryInfo,subCategoryInfo));
            }
            return info;


        }
        /// <summary>
        /// Gets a Dictionary of all Categories
        /// </summary>
        /// <returns>Dictionary of Category IDs and Category Names</returns>
        public Dictionary<int, string> GetCategories()
        {
            Dictionary<string, string> p = new Dictionary<string, string>();
            Dictionary<int, string> categories = new Dictionary<int, string>();
            XmlDocument x = new XmlDocument();


            if (!IsValidSession())
                throw new InvalidOperationException("Invalid Session.  You must logon first");

            p.Add("SessionID", _sessionKey);

            string s = SmugMugRequest(true, false, "smugmug.categories.get", p);

            if (IsRESTError(s, out x))
                throw new SmugMugApiException(x);

            XmlNodeList nodes = x.GetElementsByTagName("Category");

            for (int i = 0; i < nodes.Count; i++)
            {
                int albumID = Convert.ToInt32(nodes[i].Attributes.GetNamedItem("id").InnerText, CultureInfo.CurrentCulture);
                string albumName = nodes[i].Attributes.GetNamedItem("Title").InnerText;
                categories.Add(albumID, albumName);
            }
            return categories;
        }
        /// <summary>
        /// GetSubCategories gets a dictionary of subcategories for a given category
        /// </summary>
        /// <param name="category">the name of the category you want to see</param>
        /// <returns>Dictionary<int, string> of subcategory IDs and subcategory names.
        /// if there aren't any subcategories, or if the category doesnt even exist, will
        /// return an empty dictionary</returns>
        public Dictionary<int, string> GetSubCategories(string category)
        {

            Dictionary<string, string> p = new Dictionary<string, string>();
            Dictionary<int, string> subcategories = new Dictionary<int, string>();
            XmlDocument x = new XmlDocument();

            if (!IsValidSession())
                throw new InvalidOperationException("Invalid Session.  You must logon first");

            int categoryID = GetCategoryIDByName(category);

            p.Add("SessionID", _sessionKey);
            p.Add("CategoryID", categoryID.ToString());

            string s = SmugMugRequest(true, false, "smugmug.subcategories.get", p);

            if (IsRESTError(s, out x)) {
                SmugMugApiException ex  = new SmugMugApiException(x);
                if (ex.ErrorCode != ERROR_NO_SUBCATEGORIES)
                    throw ex;
            }
            

            XmlNodeList nodes = x.GetElementsByTagName("SubCategory");

            for (int i = 0; i < nodes.Count; i++)
            {
                int subCategoryID = Convert.ToInt32(nodes[i].Attributes.GetNamedItem("id").InnerText, CultureInfo.CurrentCulture);
                string subCategoryName = nodes[i].Attributes.GetNamedItem("Title").InnerText;
                subcategories.Add(subCategoryID, subCategoryName);
            }

            return subcategories;

        }
        /// <summary>
        /// Creates an album
        /// </summary>
        /// <param name="albumArguments"></param>
        /// <returns>SmugMugAlbumInfo about the created album</returns>
        public SmugMugAlbumInfo CreateAlbum(CreateAlbumArguments albumArguments)
        {

            SmugMugAlbumInfo returnInfo;


            Dictionary<string, string> parameters = new Dictionary<string, string>();

            if (!IsValidSession())
                throw new InvalidOperationException("Invalid session key.  You must login first");

            parameters.Add("Title", albumArguments.Name);

            int categoryID = GetCategoryIDByName(albumArguments.Category);
            parameters.Add("CategoryID", Convert.ToString(categoryID));


            if (!String.IsNullOrEmpty(albumArguments.SubCategory))
                parameters.Add("SubCategoryID", Convert.ToString(GetSubCategoryIDByName(categoryID, albumArguments.SubCategory)));


            if (!String.IsNullOrEmpty(albumArguments.Description))
                parameters.Add("Description", albumArguments.Description);

            if (!albumArguments.Public)
                parameters.Add("Public", "0");

            if (albumArguments.AllowComments)
                parameters.Add("comments", "1");

            string results = SmugMugRequest(true, false, "smugmug.albums.create", parameters);

            XmlDocument x;
            if (IsRESTError(results, out x))
            {
                throw new SmugMugApiException(x);
            }
            else
            {
                XmlDocument albumInfo = new XmlDocument();
                albumInfo.LoadXml(results);

                int AlbumID = Convert.ToInt32(albumInfo.SelectSingleNode("/rsp/Album").Attributes.GetNamedItem("id").InnerText);
                string AlbumKey = albumInfo.SelectSingleNode("/rsp/Album").Attributes.GetNamedItem("Key").InnerText;
                            

                returnInfo = new SmugMugAlbumInfo(AlbumID, AlbumKey, albumArguments.Name);
                Trace.WriteLine("new album id = " + AlbumID);
                _albums = null;
            }

            return returnInfo;

        }
        /// <summary>
        /// Uploads images to an album
        /// </summary>
        /// <param name="fileList">List of strings with file names</param>
        /// <param name="arguments">
        /// a PostPicturesToSmugMugArguments object that is filled out properly
        /// 
        /// </param>
        public SmugMugImagesInfo PostPicturesToSmugMugWithReturnInfo(List<string> fileList, PostPicturesToSmugMugArguments arguments)
        {

            int item = 0;
            if (!IsValidSession())
                throw new InvalidOperationException("You must login before posting pictures");

            SmugMugImageInfo[] imagesInfo = new SmugMugImageInfo[fileList.Count];

            int imageCount = 0;

            foreach (string s in fileList)
            {
                Trace.WriteLine("posting " + s);
                if (!File.Exists(s))
                    throw new FileNotFoundException("cant find " + s + " in PostPicturesToSmugMugWithReturnInfo");

                Trace.WriteLine("Item " + item.ToString() + " of " + fileList.Count);
                string results = SmugMugBinaryUpload(s, arguments);

                XmlDocument x;
                if (IsRESTError(results, out x))
                    throw new SmugMugApiException(x);

                SmugMugImageInfo info = new SmugMugImageInfo(x);
                imagesInfo[imageCount++] = info;

                if (arguments.DeleteAfterUpload)
                {
                    try
                    {
                        Trace.WriteLine("Deleting " + s);
                        File.Delete(s);
                    }
                    catch (Exception e)
                    {
                        //dont do anything
                        Trace.WriteLine("Exception deleting file in PostPicturesToSmugMugWithReturnInfo. " + e.ToString());
                    }
                }
            }
            return new SmugMugImagesInfo(imagesInfo);
        }
        /// <summary>
        /// Uploads images to an album
        /// </summary>
        /// <param name="fileList">List of strings with file names</param>
        /// <param name="arguments">
        /// a PostPicturesToSmugMugArguments object that is filled out properly
        /// 
        /// </param>
        public void PostPicturesToSmugMug(List<string> fileList, PostPicturesToSmugMugArguments arguments)
        {

            int item = 0;
            if (!IsValidSession())
                throw new InvalidOperationException("You must login before posting pictures");

            
            foreach (string s in fileList)
            {
                Trace.WriteLine("posting " + s);
                if (!File.Exists(s))
                    throw new FileNotFoundException("cant find " + s + " in postPicturesToSmugMug");

                Trace.WriteLine("Item " + item.ToString() + " of " + fileList.Count);
                string results = SmugMugBinaryUpload(s, arguments);

                XmlDocument x;
                if (IsRESTError(results, out x))
                    throw new SmugMugApiException(x);



                if (arguments.DeleteAfterUpload) {
                    try
                    {
                        Trace.WriteLine("Deleting " + s);
                        File.Delete(s);
                    }
                    catch (Exception e){ 
                        //dont do anything
                        Trace.WriteLine("Exception deleting file in PostPicturesToSmugMug. "+ e.ToString());
                    }
                }
            }
        }
        /// <summary>
        /// Gets images from an album
        /// </summary>
        /// <param name="arguments">
        /// a GetImagesArguments with the appropriate arguments:
        /// <list>
        /// <item>AlbumName<description>the name of the album to get</description></item>
        /// <item>DownloadLocation<description>Where you want the files to be downloaded to.
        /// DownloadLocation will not be verified, and if it's not writeable, call will fail.
        /// </description></item>
        /// 
        /// </list>
        /// </param>
        public void DownloadImages(DownloadImagesArguments arguments)
        {

            XmlDocument x = new XmlDocument();



            
            List<int> imageIDList = new List<int>();
            string albumName = arguments.AlbumName;
            string downloadLocation = arguments.DownloadLocation;

            if (string.IsNullOrEmpty(albumName))
                throw new ArgumentNullException("AlbumName cannot be null");

            if (string.IsNullOrEmpty(downloadLocation))
                throw new ArgumentNullException("DownloadLocation cannot be null");

            SmugMugAlbumInfo albumInfo = GetAlbumInfoByName(albumName);

            if (!IsValidSession())
                throw new InvalidOperationException("Invalid Session. You must logon first.");
            
            Dictionary<string, string> parameters = new Dictionary<string, string>();
            parameters.Add("AlbumID", Convert.ToString(albumInfo.AlbumID));
            parameters.Add("AlbumKey", Convert.ToString(albumInfo.AlbumKey));

            string result = SmugMugRequest(true, false, "smugmug.images.get", parameters);

            if (IsRESTError(result, out x))
                throw new SmugMugApiException(x);

            XmlNodeList nodes = x.GetElementsByTagName("Image");

            for (int i = 0; i < nodes.Count; i++)
            {
                int imageID = Convert.ToInt32(nodes[i].Attributes.GetNamedItem("id").InnerText, CultureInfo.CurrentCulture);
                string imageKey = nodes[i].Attributes.GetNamedItem("Key").InnerText;


                Dictionary<string, string> imageURLParameters = new Dictionary<string, string>();
                imageURLParameters.Add("ImageID", imageID.ToString(CultureInfo.CurrentCulture));
                imageURLParameters.Add("ImageKey", imageKey);

                string imageURLResult = SmugMugRequest(true, false, "smugmug.images.getURLs", imageURLParameters);

                XmlDocument imageLocationXML = new XmlDocument();
                if (IsRESTError(imageURLResult, out imageLocationXML))
                    throw new SmugMugApiException(imageLocationXML);

                string imageURL = imageLocationXML.SelectSingleNode("/rsp/Image").Attributes.GetNamedItem("OriginalURL").InnerText;

                DownloadFile(imageURL, downloadLocation);

            }

        }
        /// <summary>
        /// Gets all of the images and information about each image given an album name
        /// </summary>
        /// <param name="arguments"></param>
        /// <returns></returns>
        public SmugMugImages GetImages(GetImagesArguments arguments)
        {

            XmlDocument x = new XmlDocument();
            List<int> imageIDList = new List<int>();
            string albumName = arguments.AlbumName;

            if (string.IsNullOrEmpty(albumName))
                throw new ArgumentNullException("AlbumName cannot be null");

            SmugMugAlbumInfo albumInfo = GetAlbumInfoByName(albumName);

            if (!IsValidSession())
                throw new InvalidOperationException("Invalid Session.  You must logon first");

            if (albumInfo != null && albumInfo.AlbumID < 0)
            {
                throw new ArgumentException(String.Format("{0} is not a valid Album.", albumName));
            }
            else
            {
                Dictionary<string, string> parameters = new Dictionary<string, string>();
                parameters.Add("AlbumID", Convert.ToString(albumInfo.AlbumID));
                parameters.Add("AlbumKey", Convert.ToString(albumInfo.AlbumKey));

                string result = SmugMugRequest(true, false, "smugmug.images.get", parameters);

                if (IsRESTError(result, out x))
                    throw new SmugMugApiException(x);

                XmlNodeList nodes = x.GetElementsByTagName("Image");

                List<SmugMugImage> returnList = new List<SmugMugImage>();

                for (int i = 0; i < nodes.Count; i++)
                {
                    int imageID = Convert.ToInt32(nodes[i].Attributes.GetNamedItem("id").InnerText, CultureInfo.CurrentCulture);
                    string imageKey = nodes[i].Attributes.GetNamedItem("Key").InnerText;

                    Dictionary<string, string> imageInfoArguments = new Dictionary<string, string>();
                    imageInfoArguments.Add("ImageID", imageID.ToString(CultureInfo.CurrentCulture));
                    imageInfoArguments.Add("ImageKey", imageKey.ToString(CultureInfo.CurrentCulture));
                    imageInfoArguments.Add("SessionID", _sessionKey.ToString(CultureInfo.CurrentCulture));

                    string imageInfoResult = SmugMugRequest(true, false, "smugmug.images.getInfo", imageInfoArguments);

                    XmlDocument imageXML = new XmlDocument();
                    if (IsRESTError(imageInfoResult, out imageXML))
                        throw new SmugMugApiException(imageXML);

                    SmugMugImageInfo imageInfo = new SmugMugImageInfo(imageXML);
                    GetEXIFArguments ex = new GetEXIFArguments(imageID, imageKey);
                    SmugMugImageEXIFData exif = GetEXIFData(ex);
                    imageInfo.EXIFData = exif;

                    returnList.Add(new SmugMugImage(new SmugMugImageInfo(imageXML), true));

                }
                return (returnList.Count > 0? new SmugMugImages(returnList):null);
            }


        }

        public SmugMugImageInfo GetImageInfo(GetImageInfoArguments arguments){

            int imageID = arguments.ImageID;
            string imageKey = arguments.ImageKey;

            if (!IsValidSession())
                throw new InvalidOperationException("Invalid Session.  Must logon first");
            

            Dictionary<string, string> imageInfoArguments = new Dictionary<string, string>();
            imageInfoArguments.Add("ImageID", imageID.ToString(CultureInfo.CurrentCulture));
            imageInfoArguments.Add("ImageKey", imageKey);
            imageInfoArguments.Add("SessionID", _sessionKey);

            string imageInfoResult = SmugMugRequest(true, false, "smugmug.images.getInfo", imageInfoArguments);

            XmlDocument imageXML = new XmlDocument();
            if (IsRESTError(imageInfoResult, out imageXML))
                throw new SmugMugApiException(imageXML);

            SmugMugImageInfo imageInfo = new SmugMugImageInfo(imageXML);
            GetEXIFArguments ex = new GetEXIFArguments(imageID, imageKey);
            SmugMugImageEXIFData exif = GetEXIFData(ex);
            imageInfo.EXIFData = exif;

            return imageInfo;
        
        }

        /// <summary>
        /// Retrieves information about images in an album, but without
        /// downloading the images themselves.
        /// </summary>
        /// <param name="arguments"></param>
        /// <returns></returns>
        public SmugMugImagesInfo GetImagesInfo(GetImagesInfoArguments arguments)
        {

            XmlDocument x = new XmlDocument();

            List<int> imageIDList = new List<int>();
            string albumName = arguments.AlbumName;

            if (string.IsNullOrEmpty(albumName))
                throw new ArgumentNullException("AlbumName cannot be null");

            SmugMugAlbumInfo albumInfo = GetAlbumInfoByName(albumName);

            if (!IsValidSession())
                throw new InvalidOperationException("Invalid Session. You must logon first.");

            if (albumInfo == null || albumInfo.AlbumID < 0)
            {
                Console.WriteLine("{0} is not a valid Album.", albumName);
                return null;
            }
            else
            {

                Dictionary<string, string> parameters = new Dictionary<string, string>();
                parameters.Add("AlbumID", Convert.ToString(albumInfo.AlbumID));
                parameters.Add("AlbumKey", albumInfo.AlbumKey);

                string result = SmugMugRequest(true, false, "smugmug.images.get", parameters);

                if (IsRESTError(result, out x))
                    throw new SmugMugApiException(x);

                XmlNodeList nodes = x.GetElementsByTagName("Image");

                List<SmugMugImageInfo> returnList = new List<SmugMugImageInfo>();

                for (int i = 0; i < nodes.Count; i++)
                {
                    int imageID = Convert.ToInt32(nodes[i].Attributes.GetNamedItem("id").InnerText, CultureInfo.CurrentCulture);
                    string imageKey = nodes[i].Attributes.GetNamedItem("Key").InnerText;

                    Dictionary<string, string> imageInfoArguments = new Dictionary<string, string>();
                    imageInfoArguments.Add("ImageID", imageID.ToString(CultureInfo.CurrentCulture));
                    imageInfoArguments.Add("ImageKey", imageKey);
                    imageInfoArguments.Add("SessionID", _sessionKey.ToString(CultureInfo.CurrentCulture));

                    string imageInfoResult = SmugMugRequest(true, false, "smugmug.images.getInfo", imageInfoArguments);

                    XmlDocument imageInfoXML = new XmlDocument();
                    if (IsRESTError(imageInfoResult, out imageInfoXML))
                        throw new SmugMugApiException(imageInfoXML);


                    SmugMugImageInfo info = new SmugMugImageInfo(imageInfoXML);
                    SmugMugImageEXIFData exif = GetEXIFData(new GetEXIFArguments(info.ImageID, info.ImageKey));
                    info.EXIFData = exif;

                    returnList.Add(info);

                }
                return new SmugMugImagesInfo(returnList);
            }

        }

        public SmugMugImageInfo SetImageInfo(SetImageInfoArguments arguments) {

            XmlDocument x = new XmlDocument();
            Dictionary<string, string> parameters = new Dictionary<string, string>();

            if (!IsValidSession())
                throw new InvalidOperationException("Invalid session.  you must logon to use this method");

            parameters.Add("SessionID", _sessionKey);
            parameters.Add("ImageID", arguments.ImageID.ToString());
            parameters.Add("ImageKey", arguments.ImageKey);

            if(arguments.AlbumName != String.Empty){
                SmugMugAlbumInfo info = GetAlbumInfoByName(arguments.AlbumName);
                parameters.Add("AlbumID", info.AlbumID.ToString());
            }

            if (arguments.Caption != string.Empty) {
                parameters.Add("Caption", arguments.Caption);
            }
            if (arguments.Keywords != string.Empty) {
                parameters.Add("Keywords", arguments.Keywords);
            }
            parameters.Add("Hidden", arguments.Hidden.ToString());

            string result = SmugMugRequest(true, true, "smugmug.images.changeSettings", parameters);

            if (IsRESTError(result, out x))
            {
                throw new SmugMugApiException(x);
            }
            else {

                GetImageInfoArguments infoArguments = new GetImageInfoArguments(arguments.ImageID, arguments.ImageKey);
                return GetImageInfo(infoArguments);
            }

            
        }

        public string GetAlbumURL(string albumName, string urlBase) {

            
            if (!urlBase.EndsWith("/"))
                urlBase += "/";

            string returnString = urlBase;

            SmugMugAlbumInfo info = GetAlbumInfoByName(albumName);
            if (info != null) {
                returnString += info.AlbumID + "_" + info.AlbumKey;
            }
            return returnString;
            
        }

        #endregion

    }
    [Serializable]
    public class SmugMugApiException : Exception
    {
        #region Private Variables
        private string _errorStatus = "";
        private string _errorMessage = "";
        private int _errorCode;
        private XmlDocument _RESTXMLReturn;
        #endregion

        #region Constructors

        public SmugMugApiException()
            : base()
        {
            //
        }

        public SmugMugApiException(string errorMessage, string errorStatus, int errorCode)
        {
            _errorCode = errorCode;
            _errorStatus = errorStatus;
            _errorMessage = errorMessage;
        }

        public SmugMugApiException(XmlDocument errorDocument)
        {
            _RESTXMLReturn = errorDocument;
            parseXML();
        }

        public SmugMugApiException(string message)
            : base(message)
        {

        }
        public SmugMugApiException(string message, Exception innerException) :
            base(message, innerException)
        {

        }

        protected SmugMugApiException(SerializationInfo info, StreamingContext context)
        {
            if (info == null)
                throw new ArgumentNullException("info");

            _errorMessage = (string)info.GetValue("AltMessage", typeof(string));
            _errorCode = (int)info.GetValue("AltCode", typeof(int));
            _errorStatus = (string)info.GetValue("AltStatus", typeof(string));

        }
        #endregion

        #region Private Methods

        private void parseXML()
        {
            try
            {
                _errorStatus = _RESTXMLReturn.DocumentElement.Attributes.GetNamedItem("stat").InnerText;
                _errorCode = Convert.ToInt32(_RESTXMLReturn.DocumentElement.SelectSingleNode("err").Attributes.GetNamedItem("code").InnerText, CultureInfo.CurrentCulture);
                _errorMessage = _RESTXMLReturn.DocumentElement.SelectSingleNode("err").Attributes.GetNamedItem("msg").InnerText;
            }
            catch { throw; }
        }
        #endregion

        #region Public Methods


        public override string ToString()
        {
            return "Execution failed. Error '" + _errorStatus + "' code " + _errorCode.ToString() + " (" + _errorMessage + ")";
        }

        public int ErrorCode
        {
            get { return _errorCode; }
        }

        public override string Message
        {
            get { return _errorMessage; }
        }

        [SecurityPermission(SecurityAction.LinkDemand,
            Flags = SecurityPermissionFlag.SerializationFormatter)]
        public override void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            if (info == null)
                throw new ArgumentNullException("info");

            info.AddValue("AltStatus", _errorStatus);
            info.AddValue("AltCode", _errorCode);
            info.AddValue("AltMessage", _errorMessage);

        }
        #endregion

    }
}
